#include "TextureManager.h"
#include <windows.h>

#include "log.h"
#include "common.h"
#include "drawing.h"
#include <math.h>

#include <string>
#include <map>
#include <deque>

#include <lightimage.h>

namespace Tmpl {

// Internal Data structures
struct OpenGLInternalTexture
{
	std::string filename;
	OpenGLTexture tex;
	int quality;
	float lastUsed;
	Pixel* buffer;
};
typedef std::map<std::string, OpenGLInternalTexture*> OpenGLTexMap;
typedef std::deque<OpenGLInternalTexture*> OpenGLTexLoadArray;

static OpenGLTexMap gTextures;
static OpenGLTexLoadArray gLoadArray;

// Syncing and threading
static HANDLE gExitEvent;
static HANDLE gLoadSemaphore;
static HANDLE gThreadHandle;
static CRITICAL_SECTION gLoadCS;

// Threaded functions
void LoadTexture(OpenGLInternalTexture* tex)
{
	const char* filename = tex->filename.c_str();
	const int quality = tex->quality;

	// Load
	Log::Print("TextureManager - Loading: %s", filename);
	OpenGLTexture& texData = tex->tex;

	// Create file handle
	FILE* fp;
	fopen_s(&fp, filename, "rb");
	if(!fp)
	{
		LOG_ERROR("TextureManager - Unable to open file: %s", filename);
		return;
	}

	// Load the pixels from file
	int format;
	Pixel* bits = (Pixel*)stbi_load_from_file(fp, &texData.w, &texData.h, &format, STBI_rgb_alpha);
	fclose(fp);
	if(!bits)
	{
		const char* reason = stbi_failure_reason();
		LOG_ERROR("TextureManager - Unable to load Image \"%s\"", filename);
		return;
	}

	// Copy image bits
	EnterCriticalSection(&gLoadCS);
	tex->buffer = MALLOC64(texData.w * texData.h, Pixel);
	for(int y=0; y<texData.h; ++y)
	{
		const Pixel* line = bits + y * texData.w;
		Pixel* dst = tex->buffer + y * texData.w;
		for(int x=0; x<texData.w; ++x) dst[x] = line[x];
	}
	LeaveCriticalSection(&gLoadCS);
	stbi_image_free(bits);
}

static DWORD WINAPI TextureManagerThreadRunner( LPVOID param )
{
	HANDLE handles[2];
	handles[0] = gExitEvent;
	handles[1] = gLoadSemaphore;
	while(1)
	{
		switch (WaitForMultipleObjects(2, handles, FALSE, INFINITE))
		{
		// Exit Event
		case WAIT_OBJECT_0:
			return TRUE;

		// Semaphore
		case WAIT_OBJECT_0+1:
			break;

		// Timeout
		case WAIT_TIMEOUT:
			break;

		// Error
		default:
			Log::Write("TextureManager - ThreadRunner - Error Occured!");
			break;
		}

		// Grab the next texture
		OpenGLInternalTexture* tex = 0;
		EnterCriticalSection(&gLoadCS);
		if (!gLoadArray.empty())
		{
			tex = gLoadArray.front();
			gLoadArray.pop_front();
		}
		LeaveCriticalSection(&gLoadCS);

		// load it
		if (tex) LoadTexture(tex);
	}
	return TRUE;
}

// Texture Manager
void TextureManager::Init()
{
	// Create Handles
	gExitEvent = CreateEvent( NULL, TRUE, FALSE, NULL);
	gLoadSemaphore = CreateSemaphore( NULL, 0, 1024, NULL );

	// Create CS
	InitializeCriticalSection( &gLoadCS );

	// Create the load thread
	gThreadHandle = CreateThread( NULL, 0, TextureManagerThreadRunner, NULL, 0, NULL );
}
void TextureManager::Exit()
{
	SetEvent(gExitEvent);
	WaitForSingleObject(gThreadHandle, INFINITE);

	// Clean up handles
	CloseHandle(gThreadHandle);
	CloseHandle(gExitEvent);
	CloseHandle(gLoadSemaphore);

	// delete CS
	DeleteCriticalSection( &gLoadCS );

	// clear the map
	OpenGLTexMap::iterator it;
	for (it = gTextures.begin(); it != gTextures.end(); ++it)
	{
		glDeleteTextures(1, &it->second->tex.texId);
		delete it->second;
	}
	gTextures.clear();

	// clear the load map
	gLoadArray.clear();
}

void TextureManager::Update(float dt)
{
	// iterate through all textures and see if we they need an update
	EnterCriticalSection(&gLoadCS);
	OpenGLTexMap::iterator it;
	for (it = gTextures.begin(); it != gTextures.end();)
	{
		OpenGLInternalTexture* tex = it->second;

		// update this texture
		tex->lastUsed += dt;

		// if we have a buffer, then that means the texture has loaded
		if (tex->buffer)
		{
			Log::Print("TextureManager - Updating: %s", tex->filename.c_str());

			// Build opengl Image
			glEnable(GL_TEXTURE_2D);
			glBindTexture(GL_TEXTURE_2D, tex->tex.texId);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex->tex.w, tex->tex.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, tex->buffer);
			glGenerateMipmap(GL_TEXTURE_2D);

			// clean up
			FREE64(tex->buffer);
			tex->buffer = 0;
			tex->lastUsed = 0.0f;
		}

		// if we have not used this texture for a long time, destroy it
		if (tex->lastUsed > 120.0f)
		{
			Log::Print("TextureManager - Deleting: %s", tex->filename.c_str());

			// clear opengl texture
			glDeleteTextures(1, &tex->tex.texId);
			tex->tex.texId = 0;

			// remove it from the map
			it = gTextures.erase(it);

			// delete the pointer
			delete tex;
		}
		else
		{
			++it;
		}
	}
	LeaveCriticalSection(&gLoadCS);
}
OpenGLTexture TextureManager::GetTexture(const char* filename, int quality)
{
	// Look if texture is already loaded in
	const std::string fileName(filename);
	OpenGLTexMap::iterator it = gTextures.find( fileName );
	if(it != gTextures.end())
	{
		// If the quality is too low, load the higher level texture now
		EnterCriticalSection(&gLoadCS);
		if (it->second->quality < quality)
		{
			it->second->quality++;
			gLoadArray.push_back(it->second);
		}

		// return this texture
		it->second->lastUsed = 0.0f;
		LeaveCriticalSection(&gLoadCS);
		ReleaseSemaphore( gLoadSemaphore, 1, NULL );
		return it->second->tex;
	}

	// Create the new empty texture
	OpenGLInternalTexture* texData = new OpenGLInternalTexture();
	texData->filename = fileName;
	texData->lastUsed = 0.0f;
	texData->quality = QUALITY_LOW;
	texData->buffer = 0;
	texData->tex.h = 0;
	texData->tex.w = 0;
	glGenTextures(1, &texData->tex.texId);

	// load the new texture
	EnterCriticalSection(&gLoadCS);
	gLoadArray.push_front(texData);

	// return and add it to the map
	gTextures.insert(OpenGLTexMap::value_type(fileName, texData));
	LeaveCriticalSection(&gLoadCS);

	// return
	ReleaseSemaphore( gLoadSemaphore, 1, NULL );
	return texData->tex;
}

};